var FMFPdxfConverter = Class.create();
FMFPdxfConverter.prototype = {
    initialize: function () {
        this.utils = new FMFPAppUtils();
        this.entityToPolyline = new FMFPentityToPolyline();
        this.layerUtils = new x_lsmcb_fm.FMFPLayerUtils();
    },

    //TODO: Bezier's. Fix ellipses

    type: 'FMFPdxfConverter',

    /**
     * Loads the attachment, parses and processes it according to the fetched layer configurations. 
     * Converts the parsed dxf information, generates whatever is mapped (facilities), layers, geoJSON's and SVG.
     * 
     * @param {String} versionSysId
     * @param {String} attachmentSysId 
     * @param {Boolean} isNewFloor 
     * @param {Array} manualConversionLayers
     * 
     * @returns {Object} FMFPOperationResult
     */
    convertAndSave: function (versionSysId, attachmentSysId, isNewFloor, manualConversionLayers) {

        var convertionSource = 'manual'; //for now still not saving the jpeg images

        if (manualConversionLayers && manualConversionLayers.length > 0)
            convertionSource = 'manual_dxf';
        else
            convertionSource = 'autocad';

        // Load the attachment
        var dxfString = this.load(attachmentSysId);
        if (!dxfString.success)
            return dxfString;
        dxfString = dxfString.result;

        // Retrieved ServiceNow configured layer information
        var configuredLayers = this.layerUtils.getConfiguredLayers(versionSysId, convertionSource, manualConversionLayers);
        if (!configuredLayers.success)
            return configuredLayers;
        configuredLayers = configuredLayers.result;

        // Parse the loaded DXF attachment
        var parsedDXF = new FMFPdxfParser(configuredLayers.regApp).parseSync(dxfString);
        if (!parsedDXF.success)
            return parsedDXF;
        parsedDXF = parsedDXF.result;

        // Order the entities according to the Layer Standard Configuration order field
        configuredLayers.layersByOrder = this.getEntitiesByLayerOrder(parsedDXF, configuredLayers.layersByOrder);

        var entities = this.denormalize(parsedDXF, configuredLayers, versionSysId, isNewFloor, convertionSource);

        // Conversion of AutoCAD data to ServiceNow processable data
        var convertedDXF = this.convert(entities.denormalized, parsedDXF.tables.layer, entities.evaluatedLSC);
        if (!convertedDXF.success)
            return convertedDXF;
        convertedDXF = convertedDXF.result;

        // Create ServiceNow layer records and save the converted data (geoJSON/SVG)
        var savedLayers = this.layerUtils.saveLayer(versionSysId, convertedDXF, entities.evaluatedLSC);
        if (!savedLayers.success)
            return savedLayers;
        savedLayers = savedLayers.result;

        return new FMFPOperationResult(true, "Operation successful.");
    },

    /**
     * Loads the DXF attachment and returns it as a string
     * 
     * @param {String} attachmentSysId
     * 
     * @returns {Object} FMFPOperationResult
     */
    load: function (attachmentSysId) {

        //sets the DXF_String property
        var result = '',
            ln = '',
            reader = new GlideTextReader(new GlideSysAttachment().getContentStream(attachmentSysId));

        while ((ln = reader.readLine()) != null)
            result += ln + '\n';

        if (result)
            return new FMFPOperationResult(true, "Attachment loaded.", result);

        return new FMFPOperationResult(false, "Attachment failed to load.");
    },

    /**
     * Receives the entities to be processed, converts and extracts the elements and bounding box.
     * Returns an array of Layer Standard Configurations with the geoJSON for each configured layer and an SVG for the image layer
     * 
     * @param {Array} entitiesArrayByOrder Ordered entities to be processed
     * @param {Object} layersTable AutoCAD layer definition details
     * @param {Object} evaluatedLSC Layer Standard Configurations
     * 
     * @returns {Object} SVG: {String}, geoJSONarr: {Array}
     */
    convert: function (entitiesArrayByOrder, layersTable, evaluatedLSC) {

        var objectContext = this,
            boundsAndElement = null;
        objectContext.bgGeoJSON = {
            type: 'FeatureCollection',
            features: []
        };

        var _entitiesReduce = entitiesArrayByOrder.reduce(function (acc, entity, i) {

            if (entity.type === 'LINE' || entity.type === 'LWPOLYLINE' || entity.type === 'POLYLINE' || entity.type === 'ELLIPSE' || entity.type === 'ARC' || entity.type === 'CIRCLE') {

                // Assign the autoCAD color
                var rgb = objectContext.getRGBForEntity(layersTable, entity);

                entity.transformMatrix = objectContext.getTransformMatrix(entity.transforms);

                // Convert the entity
                boundsAndElement = objectContext.entityToBoundsAndElement(entity);

                if (boundsAndElement) {

                    var element = boundsAndElement.element;
                    var bbox = boundsAndElement.bbox;

                    // Generate layer's geoJSON
                    if (evaluatedLSC.geojson) {
                        evaluatedLSC.geojson.forEach(function (layerConfig) {
                            layerConfig.fileLayers.forEach(function (layer) {

                                if (entity.layer === layer) {
                                    var feature = objectContext.convertToGeoJSON(entity, element, rgb);

                                    if (feature) {
                                        if (entity.insertHandle) {
                                            layerConfig.geoJSON.inserts[entity.insertHandle] =
                                                layerConfig.geoJSON.inserts[entity.insertHandle] || [];
                                            layerConfig.geoJSON.inserts[entity.insertHandle].push(feature);
                                        }
                                        else
                                            layerConfig.geoJSON.features.push(feature);
                                    }
                                }
                            });
                        });
                    }

                    // The LSC with the target format 'image' will also generate an SVG
                    if (evaluatedLSC.image) {

                        evaluatedLSC.image.forEach(function (layerConfig) {
                            layerConfig.fileLayers.forEach(function (layer) {
                                if (entity.layer === layer) {

                                    layerConfig.geoJSON.features.push(objectContext.convertToGeoJSON(entity, element, rgb));
                                    objectContext.convertToSVG(acc, element, bbox, rgb, entity);
                                }
                            });
                        });
                    }
                }
            }
            return acc;

        }, {
            bbox: new FMFPconverterViewBox(),
            elements: []
        }),
            bbox = _entitiesReduce.bbox,
            elements = _entitiesReduce.elements;

        var viewBox = bbox.min.x === Infinity ? {
            x: 0,
            y: 0,
            width: 0,
            height: 0
        } : {
                x: bbox.min.x,
                y: -bbox.min.y - (bbox.max.y - bbox.min.y),
                width: bbox.max.x - bbox.min.x,
                height: bbox.max.y - bbox.min.y
            };

        if (elements.length === 0)
            gs.warn('A problem has ocurred during the SVG generation');

        var result = {
            SVG: objectContext.utils.format(
                '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="{0} {1} {2} {3}" width="100%" height="100%">\n' +
                '<g stroke="#000000" stroke-width="0.03" fill="none" transform="rotate(-90)">\n' +
                '{4}' +
                '</g>\n' +
                '</svg>', viewBox.x, viewBox.y, viewBox.width, viewBox.height, elements.join("\n")),

            geoJSONarr: evaluatedLSC.geojson
        };

        if (result.geoJSONarr.length === 0)
            new FMFPOperationResult(false, 'Failed to create JSON object during the conversion.')

        return new FMFPOperationResult(true, 'Operation Successful', result)
    },

    /**
     * Returns the converted entity structure to be read by vue
     * 
     * @param {Object} entity Parsed Entity
     * @param {Array} element Array containing each path coordinate
     * @param {Array} rgb Color
     * 
     * @returns {Object} geoJSON
     */
    convertToGeoJSON: function (entity, element, rgb) {

        return {
            type: 'Feature',
            properties: entity.mappingProperties || {},
            color: rgb,
            geometry: {
                type: entity.type,
                coordinates: element
            }
        }
    },

    /**
     * Group the entities' elements into one SVG path and set its corresponding autoCAD color
     * 
     * @param {Object} acc 
     * @param {Array} elements array of path commands and coordinates
     * @param {Object} bbox Bounding Box (FMFPconverterViewBox())
     * @param {Array} rgb Color
     */
    convertToSVG: function (acc, elements, bbox, rgb, entity) {
        var path = '';
        elements.forEach(function (item) {
            Object.keys(item).forEach(function (key) {
                if (path != "")
                    path += " ";

                path += key + item[key].join(' ');
            });
        });

        if (entity.type == 'ELLIPSE') {

            elementPath = '<path startAngle="' + entity.startAngle + '" endAngle="' + entity.endAngle + '" handle="' + entity.handle + '" d="' + path + '"></path>';
        }
        else
            elementPath = '<path d="' + path + '"></path>';

        acc.bbox.expandByPoint(bbox.min);
        acc.bbox.expandByPoint(bbox.max);

        acc.elements.push(this.utils.format('<g stroke="{0}">{1}</g>', this.rgbToColorAttribute(rgb), elementPath));
    },


    /**
     * Switch the appropriate function on entity type.
     * 
     * @param {Object} entity
     * 
     * @returns {Object} element: converted element, bbox: bounding box of given element
     */
    entityToBoundsAndElement: function (entity) {
        var objectContext = this;
        switch (entity.type) {
            case 'CIRCLE':
                return objectContext.circle(entity);
            case 'ELLIPSE':
                return objectContext.ellipse(entity);
            case 'ARC':
                return objectContext.arc(entity);
            case 'SPLINE':
                if (entity.degree === 2 || entity.degree === 3) {
                    try {
                        return objectContext.bezier(entity);
                    } catch (err) {
                        return objectContext.polyline(entity);
                    }
                } else
                    return objectContext.polyline(entity);
            case 'LINE':
            case 'LWPOLYLINE':
            case 'POLYLINE':
                return objectContext.polyline(entity);
            default:
                //logger.warn('entity type not supported in SVG rendering:', entity.type);
                return null;
        }
    },

    /**
     * Retrieve the color from the layers table according to the entity's layer
     * 
     * @param {Object} layersTable AutoCAD layer definition details
     * @param {Object} entity 
     * 
     * @returns {Array} RGB color values
     */
    getRGBForEntity: function (layersTable, entity) {
        var layerTable = layersTable.layers[entity.layer];

        if (layerTable.color)
            return layerTable.color;

        return [0, 0, 0];
    },

    /**
     * Convert an RGB array to a CSS string definition.
     * Converts white lines to black as the default.
     * 
     * @param {Array} rgb 
     * 
     * @returns {String}
     */
    rgbToColorAttribute: function (rgb) {
        if (rgb[0] === 255 && rgb[1] === 255 && rgb[2] === 255) {
            return 'rgb(0, 0, 0)';
        } else {
            return this.utils.format('rgb({0}, {1}, {2})', rgb[0], rgb[1], rgb[2]);
        }
    },

    /**
     * Rotate a points by the given angle.
     * 
     * @param {Object} point 
     * @param {Number} angle 
     */
    rotate: function (point, angle) {
        return {
            x: point.x * Math.cos(angle) - point.y * Math.sin(angle),
            y: point.y * Math.cos(angle) + point.x * Math.sin(angle)
        };
    },

    /**
     * Building and Floor generation/connection
     * Replace AutoCAD INSERT blocks by the block's definition entities and parse parent transform
     * For every entity, evaluate and apply the mappings connected to SN's LSC layer entries
     * 
     * @param {*} parsedDXF 
     * @param {*} configuredLayers 
     * @param {*} versionSysId 
     * @param {*} isNewFloor 
     * @param {*} convertionSource 
     */
    denormalize: function (parsedDXF, configuredLayers, versionSysId, isNewFloor, convertionSource) {

        var blocksByName = parsedDXF.blocks.reduce(function (acc, b) {
            acc[b.name] = b;
            return acc;
        }, {}),
            objectContext = this,
            layerStandardConfig = configuredLayers.layerStandardConfig,
            entitiesByLayer = configuredLayers.layersByOrder,
            dxfMapper = new FMFPdxfMapper(),
            entitiesArrayByOrder = [];

        // Generate an array with all the entities to be processed
        // (entitiesByLayer's properties have been generated according to the LSC order, so this array will be processed by order)
        Object.keys(entitiesByLayer).forEach(function (layerName) {
            entitiesByLayer[layerName].forEach(function (entity) {
                entitiesArrayByOrder.push(entity);
            });
        });

        // Generate/connect building and floor according to the information present on the entity whose LSC has the lowest order
        if (entitiesArrayByOrder[0] &&
            entitiesArrayByOrder[0].extendedData &&
            entitiesArrayByOrder[0].extendedData.customStrings &&
            entitiesArrayByOrder[0].extendedData.customStrings[1] &&
            entitiesArrayByOrder[0].extendedData.customStrings[2])
            dxfMapper.generateFacilities(versionSysId, isNewFloor,
                entitiesArrayByOrder[0].extendedData.customStrings[1], entitiesArrayByOrder[0].extendedData.customStrings[2]);

        var evaluatedLSC = {};

        // Check and run the LSC's mappings. Replace 'INSERT' entities with the entities from the corresponding autoCAD block
        var gatherEntities = function (entitiesArrayByOrder, transforms) {
            var current = [];

            // Mappings and related entities
            entitiesArrayByOrder.forEach(function (e) {
                layerStandardConfig.forEach(function (lsc) {

                    if (convertionSource === 'autocad') {
                        var evaluatedTrue = false;

                        lsc.fileLayers.forEach(function (layerName) {

                            // Mapping condition evaluation
                            if (e.layer == layerName && dxfMapper.evaluateCondition(e, lsc.condition)) {
                                evaluatedTrue = true;

                                var condensedElement = JSON.parse(JSON.stringify(e));
                                var arrRelatedEntities = [];

                                // Check for relevant related entities and push it to the parent entity to be read by the mapper
                                if (e.extendedData && e.extendedData.relatedEntities) {

                                    e.extendedData.relatedEntities.forEach(function (relatedEntity) {

                                        var relatedEntityFound = entitiesArrayByOrder.filter(function (currEntity) {
                                            return currEntity.handle === relatedEntity;
                                        });
                                        arrRelatedEntities.push(relatedEntityFound[0]);
                                    });

                                    if (!gs.nil(condensedElement.extendedData.relatedEntities))
                                        condensedElement.extendedData.relatedEntities = arrRelatedEntities;
                                }

                                // Setting the properties returned by the mapping
                                var mappingProperties = dxfMapper.applyMappings(lsc.sysID, lsc.objectOperation, condensedElement);
                                if (mappingProperties && mappingProperties.coalesceFields && mappingProperties.coalesceFields !== {})
                                    e['mappingProperties'] = mappingProperties;
                            }
                        });
                        if (evaluatedTrue && !lsc.used)
                            lsc.used = true;
                    }
                    else if (!lsc.used)
                        lsc.used = true;
                });

                // Replace the 'INSERT' entity (parent) by the corresponding autoCAD block (child entities)
                if (e.type === 'INSERT') {
                    var insert = e;
                    var block = blocksByName[insert.name];
                    if (!block)
                        return;

                    var t = {
                        position: {
                            x: -block.position.x + insert.position.x,
                            y: -block.position.y + insert.position.y,
                        },
                    };

                    // Copy parent entity properties into child entity 
                    if (insert.mappingProperties) t.mappingProperties = insert.mappingProperties;
                    if (insert.xScale) t.xScale = insert.xScale;
                    if (insert.yScale) t.yScale = insert.yScale;
                    if (insert.zScale) t.zScale = insert.zScale;
                    if (insert.rotation) t.rotation = insert.rotation;
                    if (insert.extrusionDirection) {
                        t.extrusionDirection = {};
                        if (insert.extrusionDirection.x) t.extrusionDirection.x = insert.extrusionDirection.x;
                        if (insert.extrusionDirection.y) t.extrusionDirection.y = insert.extrusionDirection.y;
                        if (insert.extrusionDirection.z) t.extrusionDirection.z = insert.extrusionDirection.z;
                    };

                    var transforms2 = transforms.slice(0);
                    transforms2.push(t);

                    // Use the insert layer
                    if (block.entities) {
                        var blockEntities = block.entities.map(function (be) {
                            var be2 = JSON.parse(JSON.stringify(be));
                            be2.layer = insert.layer;
                            if (insert.handle)
                                be2.insertHandle = insert.handle;

                            return be2;
                        });

                        current = current.concat(gatherEntities(blockEntities, transforms2));
                    }

                } else {
                    // Top-level entity. Clone and add the transforms
                    // The transforms are reversed so they occur in
                    // order of application - i.e. the transform of the
                    // top-level insert is applied last
                    var e2 = JSON.parse(JSON.stringify(e));
                    e2.transforms = transforms.slice().reverse();

                    current.push(e2);
                }
            });
            return current;
        };


        var runDenormalize = function (entities, transforms) {
            var entities = gatherEntities(entities, transforms);

            layerStandardConfig.forEach(function (lsc) {
                if (lsc.used) {
                    if (!evaluatedLSC[lsc.targetFormat]) {
                        evaluatedLSC[lsc.targetFormat] = [];
                    }

                    evaluatedLSC[lsc.targetFormat].push(lsc);
                }
            });

            return entities;
        };

        return {
            denormalized: runDenormalize(parsedDXF.entities, []),
            entitiesArrayByOrder: entitiesArrayByOrder,
            evaluatedLSC: evaluatedLSC
        }
    },

    /**
     * Order the entities according to the Layer Standard Configuration order field
     * Run through all the entities, if an entity's layer is present in layersByOrder push the entity to the corresponding layer array value
     * 
     * @param {Object} parsedDXF
     * @param {Object} layersByOrder 
     */
    getEntitiesByLayerOrder: function (parsedDXF, layersByOrder) {

        parsedDXF.entities.forEach(function (entity) {
            if (layersByOrder[entity.layer])
                layersByOrder[entity.layer].push(entity)
        })
        return layersByOrder;
    },

    /**
     * BEZIER conversion
     * 
     * @param {Object} entity 
     * 
     * @returns {Object} element: converted element, bbox: bounding box of given element
     */
    bezier: function (entity) {
        var objectContext = this;
        var bbox = new FMFPconverterViewBox();
        entity.controlPoints.forEach(function (p) {
            bbox = bbox.expandByPoint(p);
        });
        var k = entity.degreeOfSplineCurve + 1;
        var piecewise = new FMFPtoPiecewiseBezier().toPiecewiseBezier(k, entity.controlPoints, entity.knotValues);
        var element = objectContext.piecewiseToPaths(k, piecewise.controlPoints, entity.transforms);

        return { bbox: bbox, element: element };
    },

    /**
     * Auxiliary method for the BEZIER conversion.
     * 
     * @param {Number} k 
     * @param {Array} controlPoints 
     * @param {Array} transforms 
     * 
     * @returns {Array} SVG commands and paths
     */
    piecewiseToPaths: function (k, controlPoints, transforms) {
        var objectContext = this,
            element = [];
        var nSegments = (controlPoints.length - 1) / (k - 1);
        var paths = [];

        var transformMatrix = objectContext.getTransformMatrix(transforms);

        for (var i = 0; i < nSegments; ++i) {
            var cp = controlPoints.slice(i * (k - 1));

            cp.forEach(function (value, i) {
                transformed = objectContext.transformXY(value.x, value.y, transformMatrix);
                i === 0 ?
                    element.push({ M: [[transformed.x, transformed.y]] }) :
                    element.push({ C: [[transformed.x, transformed.y]] });

            });
        }
        return paths;
    },

    /**
     * ARC conversion. 
     * An ARC is an ellipse with equal radii
     * 
     * @param {Object} entity 
     * 
     * @returns {Object} element: converted element, bbox: bounding box of given element
     */
    arc: function (entity) {
        var objectContext = this;

        // In DXF an extrusionZ value of -1 denote a tranform around the Y axis.
        if (entity.extrusionDirection && entity.extrusionDirection.z) {
            var flipMatrix = objectContext.getFlipMatrix(entity.transforms);
            var fliped = objectContext.transformXY(entity.center.x, entity.center.y, flipMatrix);
            entity.center.x = fliped.x;
            entity.center.y = fliped.y;
        }

        var _ellipseOrArc = objectContext.ellipseOrArc(
            entity.center.x, entity.center.y,
            entity.radius, entity.radius,
            entity.startAngle, entity.endAngle,
            0, entity.extrusionDirection && entity.extrusionDirection.z === -1,
            entity.transformMatrix,
            entity.transforms
        );

        return _ellipseOrArc;
    },

    /**
     * Polyline conversion.
     * 
     * @param {Object} entity 
     * 
     * @returns {Object} element: converted element, bbox: bounding box of given element
     */
    polyline: function (entity) {
        var objectContext = this,
            element = [];

        var vertices = objectContext.entityToPolyline.toPolyline(entity);
        var bbox = vertices.reduce(function (acc, _ref) {
            var x = _ref[0],
                y = _ref[1];
            return acc.expandByPoint({
                x: x,
                y: y
            });
        }, new FMFPconverterViewBox());

        // X and Y are swapped due to leaflet interpretation
        vertices.forEach(function (point, i) {

            transformed = objectContext.transformXY(point[0], point[1], entity.transformMatrix);

            i === 0 ?
                element.push({ M: [[transformed.y, transformed.x]] }) :
                element.push({ L: [[transformed.y, transformed.x]] });
        });

        // Empirically it appears that flipping horizontally does not apply to polyline

        return { bbox: bbox, element: element };
    },

    /**
     * ELLIPSE conversion.
     * An ELLIPSE is defined by the major axis, convert to X and Y radius with a rotation angle
     * 
     * @param {Object} entity 
     * 
     * @returns {Object} element: converted element, bbox: bounding box of given element
     */
    ellipse: function (entity) {
        var objectContext = this, flipX = false;
        var rx = Math.sqrt(entity.majorAxisEndPoint.x * entity.majorAxisEndPoint.x + entity.majorAxisEndPoint.y * entity.majorAxisEndPoint.y);
        var ry = entity.axisRatio * rx;
        var majorAxisRotation = -Math.atan2(-entity.majorAxisEndPoint.y, entity.majorAxisEndPoint.x);

        // gs.info('entity handle: ' + entity.handle + '\n rx: ' + rx + '\n ry: ' + ry)

        // In DXF an extrusionZ value of -1 denote a X or Y axis flip
        if (entity.extrusionDirection && entity.extrusionDirection.z == -1) {
            // var flipMatrix = objectContext.getFlipMatrix(entity.transforms);
            // var fliped = objectContext.transformXY(entity.center.x, entity.center.y, flipMatrix);
            // entity.center.x = fliped.x;
            // entity.center.y = fliped.y;
            entity.startAngle = entity.startAngle - Math.PI;
            entity.endAngle = entity.endAngle - Math.PI;
        }

        var _ellipseOrArc = objectContext.ellipseOrArc(
            entity.center.x, entity.center.y,
            rx, ry,
            entity.startAngle, entity.endAngle,
            majorAxisRotation,
            flipX,
            entity.transformMatrix,
            entity.transform
        );

        return _ellipseOrArc;
    },

    /**
     * Auxiliary conversion for Ellipses and Arcs
     * 
     * @param {Number} cx 
     * @param {Number} cy 
     * @param {Number} rx 
     * @param {Number} ry 
     * @param {Number} startAngle 
     * @param {Number} endAngle 
     * @param {Number} rotationAngle 
     * @param {Boolean} flipX 
     * @param {Array} transformMatrix 
     * @param {Array} transforms 
     * 
     * @returns {Object} element: converted element, bbox: bounding box of given element
     */
    ellipseOrArc: function (cx, cy, rx, ry, startAngle, endAngle, rotationAngle, flipX, transformMatrix, transforms) {
        var objectContext = this;
        var bbox = [
            { x: rx, y: ry },
            { x: rx, y: ry },
            { x: -rx, y: -ry },
            { x: -rx, y: ry }
        ].reduce(function (acc, p) {
            var rotated = objectContext.rotate(p, rotationAngle);
            acc.expandByPoint({
                x: cx + rotated.x,
                y: cy + rotated.y
            });
            return acc;
        }, new FMFPconverterViewBox());

        var element = [];
        var elementRotation = rotationAngle / Math.PI * 90;

        if ((Math.abs(startAngle - endAngle) < 1e-9) || (Math.abs(startAngle - endAngle + Math.PI * 2) < 1e-9)) {

            transformed = objectContext.transformXY(cx, cy, transformMatrix);

            element.push({ M: [[transformed.y, transformed.x - rx]] });
            element.push({
                A: [
                    [ry, rx],
                    [elementRotation],
                    [1, 0],
                    [ry * 2, 0]
                ]
            });
            element.push({
                A: [
                    [ry, rx],
                    [elementRotation],
                    [1, 0],
                    [-(ry * 2), 0]
                ]
            });

            return { bbox: bbox, element: element };

        } else {
            var startOffset = objectContext.rotate({
                x: Math.cos(startAngle) * rx,
                y: Math.sin(startAngle) * ry
            }, rotationAngle);
            var startPoint = {
                x: cx + startOffset.x,
                y: cy + startOffset.y
            };
            var endOffset = objectContext.rotate({
                x: Math.cos(endAngle) * rx,
                y: Math.sin(endAngle) * ry
            }, rotationAngle);
            var endPoint = {
                x: cx + endOffset.x,
                y: cy + endOffset.y
            };
            var adjustedEndAngle = endAngle < startAngle
                ? endAngle + Math.PI * 2
                : endAngle;

            var largeArcFlag = adjustedEndAngle - startAngle < Math.PI ? 0 : 1;

            tStartPoints = objectContext.transformXY(startPoint.x, startPoint.y, transformMatrix);
            tEndPoints = objectContext.transformXY(endPoint.x, endPoint.y, transformMatrix);

            var sweep = 0;

            if (transforms &&
                transforms[0] &&
                transforms[0].xScale &&
                transforms[0].xScale == -1)
                sweep = 1;

            element.push({ M: [[tStartPoints.y, tStartPoints.x]] });
            element.push({
                A: [
                    [ry, rx],
                    [elementRotation],
                    [largeArcFlag],
                    [sweep],
                    [tEndPoints.y, tEndPoints.x]
                ]
            });

            return { bbox: bbox, element: element };
        }
    },

    /**
     * CIRCLE conversion
     * 
     * @param {Object} entity 
     * 
     * @returns {Object} element: converted element, bbox: bounding box of given element
     */
    circle: function (entity) {
        var objectContext = this,
            element = [];

        var bbox = new FMFPconverterViewBox().expandByPoint({
            x: entity.center.x + entity.radius,
            y: entity.center.y + entity.radius
        }).expandByPoint({
            x: entity.center.x - entity.radius,
            y: entity.center.y - entity.radius
        });

        // In DXF an extrusionZ value of -1 denote a tranform around the Y axis.
        if (entity.extrusionDirection && entity.extrusionDirection.z) {
            var flipMatrix = objectContext.getFlipMatrix(entity.transforms);
            var fliped = objectContext.transformXY(entity.center.x, entity.center.y, flipMatrix);
            entity.center.x = fliped.x;
            entity.center.y = fliped.y;
        }

        transformed = objectContext.transformXY(entity.center.x, entity.center.y, entity.transformMatrix);

        element.push({ M: [[transformed.y, transformed.x - entity.radius]] });
        element.push({
            A: [
                [entity.radius, entity.radius],
                [0],
                [1, 0],
                [transformed.y, transformed.x + entity.radius]
            ]
        });
        element.push({
            A: [
                [entity.radius, entity.radius],
                [0],
                [1, 0],
                [transformed.y, transformed.x - (entity.radius)]
            ]
        });

        return {
            bbox: bbox,
            element: element
        };
    },


    /**
     * Generates a transform matrix to flip the element
     * 
     * @param {Array} transforms 
     * 
     * @returns {Array} transformed matrix
     */
    getFlipMatrix: function (transforms) {
        var matrices = transforms.map(function (transform) {
            // Create the transformation matrix
            var sx = transform.xScale || 1;
            var sy = transform.yScale || 1;
            var angle = (transform.rotation || 0) * (Math.PI / 180); // deg->rad
            var cos = Math.cos;

            return [-sx * cos(angle), 0, 0, sy * cos(angle), 0, 0];
        });

        return matrices;
    },

    /**
     * Transform the bounding box and the element by the given transforms
     * 
     * @param {Array} transforms 
     * 
     * @returns {Array}transformed matrix
     */
    getTransformMatrix: function (transforms) {

        var matrices = transforms.map(function (transform) {

            // Create the transformation matrix
            var tx = transform.position.x || 0;
            var ty = transform.position.y || 0;
            var sx = transform.xScale || 1;
            var sy = transform.yScale || 1;
            var angle = (transform.rotation || 0) / 180 * Math.PI;
            var cos = Math.cos,
                sin = Math.sin;
            var a, b, c, d, e, f;

            // In DXF an extrusionZ value of -1 denote a tranform around the Y axis.
            if (transform.extrusion && transform.extrusion.z === -1) {
                a = -sx * cos(angle);
                b = sx * sin(angle);
                c = sy * sin(angle);
                d = sy * cos(angle);
                e = -tx;
                f = ty;
            } else {
                a = sx * cos(angle);
                b = sx * sin(angle);
                c = -sy * sin(angle);
                d = sy * cos(angle);
                e = tx;
                f = ty;
            }

            return [a, b, c, d, e, f];
        });

        return matrices;
    },

    /**
     * Calculates the transform value for x and y based on a transform matrix
     * 
     * @param {Number} x 
     * @param {Number} y 
     * @param {Array} transformMatrix
     * 
     * @returns {Object} Transformed x, y coordinates
     */
    transformXY: function (x, y, transformMatrix) {
        transformMatrix.reverse();
        transformMatrix.forEach(function (ref) {
            var a = ref[0],
                b = ref[1],
                c = ref[2],
                d = ref[3],
                e = ref[4],
                f = ref[5];

            var pX = x, pY = y;

            x = a * pX + c * pY + e;
            y = b * pX + d * pY + f;
        });

        return { 'x': x, 'y': y };
    },
};